package com.opages.mvc.adapter.common.properties.bind;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.*;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.lang.UsesJava8;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.validation.*;

import java.beans.PropertyEditor;
import java.lang.reflect.Field;
import java.util.*;

/**
 * @author daizhong.liu
 * @create 2020-6-15 16:45
 **/
public class DataBinder implements PropertyEditorRegistry, TypeConverter {

    /** Default object name used for binding: "target" */
    public static final String DEFAULT_OBJECT_NAME = "target";

    /** Default limit for array and collection growing: 256 */
    public static final int DEFAULT_AUTO_GROW_COLLECTION_LIMIT = 256;


    /**
     * We'll create a lot of DataBinder instances: Let's use a static logger.
     */
    protected static final Log logger = LogFactory.getLog(org.springframework.validation.DataBinder.class);

    private static Class<?> javaUtilOptionalClass = null;

    static {
        try {
            javaUtilOptionalClass =
                    ClassUtils.forName("java.util.Optional", org.springframework.validation.DataBinder.class.getClassLoader());
        }
        catch (ClassNotFoundException ex) {
            // Java 8 not available - Optional references simply not supported then.
        }
    }


    private final Object target;

    private final String objectName;

    private AbstractPropertyBindingResult bindingResult;

    private SimpleTypeConverter typeConverter;

    private boolean ignoreUnknownFields = true;

    private boolean ignoreInvalidFields = false;

    private boolean autoGrowNestedPaths = true;

    private int autoGrowCollectionLimit = DEFAULT_AUTO_GROW_COLLECTION_LIMIT;


    private ConversionService conversionService;

    private MessageCodesResolver messageCodesResolver;

    private BindingErrorProcessor bindingErrorProcessor = new DefaultBindingErrorProcessor();

    private final List<Validator> validators = new ArrayList<Validator>();


    /**
     * Create a new DataBinder instance, with default object name.
     * @param target the target object to bind onto (or {@code null}
     * if the binder is just used to convert a plain parameter value)
     * @see #DEFAULT_OBJECT_NAME
     */
    public DataBinder(Object target) {
        this(target, DEFAULT_OBJECT_NAME);
    }

    /**
     * Create a new DataBinder instance.
     * @param target the target object to bind onto (or {@code null}
     * if the binder is just used to convert a plain parameter value)
     * @param objectName the name of the target object
     */
    public DataBinder(Object target, String objectName) {
        if (target != null && target.getClass() == javaUtilOptionalClass) {
            this.target = DataBinder.OptionalUnwrapper.unwrap(target);
        }
        else {
            this.target = target;
        }
        this.objectName = objectName;
    }


    /**
     * Return the wrapped target object.
     */
    public Object getTarget() {
        return this.target;
    }

    /**
     * Return the name of the bound object.
     */
    public String getObjectName() {
        return this.objectName;
    }

    /**
     * Set whether this binder should attempt to "auto-grow" a nested path that contains a null value.
     * <p>If "true", a null path location will be populated with a default object value and traversed
     * instead of resulting in an exception. This flag also enables auto-growth of collection elements
     * when accessing an out-of-bounds index.
     * <p>Default is "true" on a standard DataBinder. Note that since Spring 4.1 this feature is supported
     * for bean property access (DataBinder's default mode) and field access.
     * @see #initBeanPropertyAccess()
     * @see org.springframework.beans.BeanWrapper#setAutoGrowNestedPaths
     */
    public void setAutoGrowNestedPaths(boolean autoGrowNestedPaths) {
        Assert.state(this.bindingResult == null,
                "DataBinder is already initialized - call setAutoGrowNestedPaths before other configuration methods");
        this.autoGrowNestedPaths = autoGrowNestedPaths;
    }

    /**
     * Return whether "auto-growing" of nested paths has been activated.
     */
    public boolean isAutoGrowNestedPaths() {
        return this.autoGrowNestedPaths;
    }

    /**
     * Return the current limit for array and collection auto-growing.
     */
    public int getAutoGrowCollectionLimit() {
        return this.autoGrowCollectionLimit;
    }

    /**
     * Initialize standard JavaBean property access for this DataBinder.
     * <p>This is the default; an explicit call just leads to eager initialization.
     * @see #initDirectFieldAccess()
     * @see #createBeanPropertyBindingResult()
     */
    public void initBeanPropertyAccess() {
        Assert.state(this.bindingResult == null,
                "DataBinder is already initialized - call initBeanPropertyAccess before other configuration methods");
        this.bindingResult = createBeanPropertyBindingResult();
    }

    /**
     * Create the {@link AbstractPropertyBindingResult} instance using standard
     * JavaBean property access.
     * @since 4.2.1
     */
    protected AbstractPropertyBindingResult createBeanPropertyBindingResult() {
        BeanPropertyBindingResult result = new BeanPropertyBindingResult(getTarget(),
                getObjectName(), isAutoGrowNestedPaths(), getAutoGrowCollectionLimit());

        if (this.conversionService != null) {
            result.initConversion(this.conversionService);
        }
        if (this.messageCodesResolver != null) {
            result.setMessageCodesResolver(this.messageCodesResolver);
        }

        return result;
    }

    /**
     * Initialize direct field access for this DataBinder,
     * as alternative to the default bean property access.
     * @see #initBeanPropertyAccess()
     * @see #createDirectFieldBindingResult()
     */
    public void initDirectFieldAccess() {
        Assert.state(this.bindingResult == null,
                "DataBinder is already initialized - call initDirectFieldAccess before other configuration methods");
        this.bindingResult = createDirectFieldBindingResult();
    }

    /**
     * Create the {@link AbstractPropertyBindingResult} instance using direct
     * field access.
     * @since 4.2.1
     */
    protected AbstractPropertyBindingResult createDirectFieldBindingResult() {
        DirectFieldBindingResult result = new DirectFieldBindingResult(getTarget(),
                getObjectName(), isAutoGrowNestedPaths());

        if (this.conversionService != null) {
            result.initConversion(this.conversionService);
        }
        if (this.messageCodesResolver != null) {
            result.setMessageCodesResolver(this.messageCodesResolver);
        }

        return result;
    }

    /**
     * Return the internal BindingResult held by this DataBinder,
     * as an AbstractPropertyBindingResult.
     */
    protected AbstractPropertyBindingResult getInternalBindingResult() {
        if (this.bindingResult == null) {
            initBeanPropertyAccess();
        }
        return this.bindingResult;
    }

    /**
     * Return the underlying PropertyAccessor of this binder's BindingResult.
     */
    protected ConfigurablePropertyAccessor getPropertyAccessor() {
        return getInternalBindingResult().getPropertyAccessor();
    }

    /**
     * Return this binder's underlying SimpleTypeConverter.
     */
    protected SimpleTypeConverter getSimpleTypeConverter() {
        if (this.typeConverter == null) {
            this.typeConverter = new SimpleTypeConverter();
            if (this.conversionService != null) {
                this.typeConverter.setConversionService(this.conversionService);
            }
        }
        return this.typeConverter;
    }

    /**
     * Return the underlying TypeConverter of this binder's BindingResult.
     */
    protected PropertyEditorRegistry getPropertyEditorRegistry() {
        if (getTarget() != null) {
            return getInternalBindingResult().getPropertyAccessor();
        }
        else {
            return getSimpleTypeConverter();
        }
    }

    /**
     * Return the underlying TypeConverter of this binder's BindingResult.
     */
    protected TypeConverter getTypeConverter() {
        if (getTarget() != null) {
            return getInternalBindingResult().getPropertyAccessor();
        }
        else {
            return getSimpleTypeConverter();
        }
    }

    /**
     * Return the BindingResult instance created by this DataBinder.
     * This allows for convenient access to the binding results after
     * a bind operation.
     * @return the BindingResult instance, to be treated as BindingResult
     * or as Errors instance (Errors is a super-interface of BindingResult)
     * @see Errors
     * @see #bind
     */
    public BindingResult getBindingResult() {
        return getInternalBindingResult();
    }


    /**
     * Set whether to ignore unknown fields, that is, whether to ignore bind
     * parameters that do not have corresponding fields in the target object.
     * <p>Default is "true". Turn this off to enforce that all bind parameters
     * must have a matching field in the target object.
     * <p>Note that this setting only applies to <i>binding</i> operations
     * on this DataBinder, not to <i>retrieving</i> values via its
     * {@link #getBindingResult() BindingResult}.
     * @see #bind
     */
    public void setIgnoreUnknownFields(boolean ignoreUnknownFields) {
        this.ignoreUnknownFields = ignoreUnknownFields;
    }

    /**
     * Return whether to ignore unknown fields when binding.
     */
    public boolean isIgnoreUnknownFields() {
        return this.ignoreUnknownFields;
    }

    /**
     * Set whether to ignore invalid fields, that is, whether to ignore bind
     * parameters that have corresponding fields in the target object which are
     * not accessible (for example because of null values in the nested path).
     * <p>Default is "false". Turn this on to ignore bind parameters for
     * nested objects in non-existing parts of the target object graph.
     * <p>Note that this setting only applies to <i>binding</i> operations
     * on this DataBinder, not to <i>retrieving</i> values via its
     * {@link #getBindingResult() BindingResult}.
     * @see #bind
     */
//    public void setIgnoreInvalidFields(boolean ignoreInvalidFields) {
//        this.ignoreInvalidFields = ignoreInvalidFields;
//    }

    /**
     * Return whether to ignore invalid fields when binding.
     */
    public boolean isIgnoreInvalidFields() {
        return this.ignoreInvalidFields;
    }


    /**
     * Return the fields that should be allowed for binding.
     * @return array of field names
     */
//    public String[] getAllowedFields() {
//        return this.allowedFields;
//    }


    /**
     * Return the fields that should <i>not</i> be allowed for binding.
     * @return array of field names
     */
//    public String[] getDisallowedFields() {
//        return this.disallowedFields;
//    }


    /**
     * Set whether to extract the old field value when applying a
     * property editor to a new value for a field.
     * <p>Default is "true", exposing previous field values to custom editors.
     * Turn this to "false" to avoid side effects caused by getters.
     * @deprecated as of Spring 4.3.5, in favor of customizing this in
     * {@link #createBeanPropertyBindingResult()} or
     * {@link #createDirectFieldBindingResult()} itself
     */
//    @Deprecated
//    public void setExtractOldValueForEditor(boolean extractOldValueForEditor) {
//        getPropertyAccessor().setExtractOldValueForEditor(extractOldValueForEditor);
//    }

    /**
     * Set the strategy to use for resolving errors into message codes.
     * Applies the given strategy to the underlying errors holder.
     * <p>Default is a DefaultMessageCodesResolver.
     * @see BeanPropertyBindingResult#setMessageCodesResolver
     * @see DefaultMessageCodesResolver
     */
    public void setMessageCodesResolver(MessageCodesResolver messageCodesResolver) {
        Assert.state(this.messageCodesResolver == null, "DataBinder is already initialized with MessageCodesResolver");
        this.messageCodesResolver = messageCodesResolver;
        if (this.bindingResult != null && messageCodesResolver != null) {
            this.bindingResult.setMessageCodesResolver(messageCodesResolver);
        }
    }

    /**
     * Set the strategy to use for processing binding errors, that is,
     * required field errors and {@code PropertyAccessException}s.
     * <p>Default is a DefaultBindingErrorProcessor.
     * @see DefaultBindingErrorProcessor
     */
    public void setBindingErrorProcessor(BindingErrorProcessor bindingErrorProcessor) {
        Assert.notNull(bindingErrorProcessor, "BindingErrorProcessor must not be null");
        this.bindingErrorProcessor = bindingErrorProcessor;
    }

    /**
     * Return the strategy for processing binding errors.
     */
    public BindingErrorProcessor getBindingErrorProcessor() {
        return this.bindingErrorProcessor;
    }

    /**
     * Set the Validator to apply after each binding step.
     * @see #addValidators(Validator...)
     * @see #replaceValidators(Validator...)
     */
    public void setValidator(Validator validator) {
        assertValidators(validator);
        this.validators.clear();
        this.validators.add(validator);
    }

    private void assertValidators(Validator... validators) {
        Assert.notNull(validators, "Validators required");
        Object target = getTarget();
        for (Validator validator : validators) {
            if (validator != null && (target != null && !validator.supports(target.getClass()))) {
                throw new IllegalStateException("Invalid target for Validator [" + validator + "]: " + target);
            }
        }
    }

    /**
     * Add Validators to apply after each binding step.
     * @see #setValidator(Validator)
     * @see #replaceValidators(Validator...)
     */
    public void addValidators(Validator... validators) {
        assertValidators(validators);
        this.validators.addAll(Arrays.asList(validators));
    }

    /**
     * Replace the Validators to apply after each binding step.
     * @see #setValidator(Validator)
     * @see #addValidators(Validator...)
     */
    public void replaceValidators(Validator... validators) {
        assertValidators(validators);
        this.validators.clear();
        this.validators.addAll(Arrays.asList(validators));
    }

    /**
     * Return the primary Validator to apply after each binding step, if any.
     */
    public Validator getValidator() {
        return (this.validators.size() > 0 ? this.validators.get(0) : null);
    }

    /**
     * Return the Validators to apply after data binding.
     */
    public List<Validator> getValidators() {
        return Collections.unmodifiableList(this.validators);
    }


    @Override
    public void registerCustomEditor(Class<?> requiredType, PropertyEditor propertyEditor) {
        getPropertyEditorRegistry().registerCustomEditor(requiredType, propertyEditor);
    }

    @Override
    public void registerCustomEditor(Class<?> requiredType, String field, PropertyEditor propertyEditor) {
        getPropertyEditorRegistry().registerCustomEditor(requiredType, field, propertyEditor);
    }

    @Override
    public PropertyEditor findCustomEditor(Class<?> requiredType, String propertyPath) {
        return getPropertyEditorRegistry().findCustomEditor(requiredType, propertyPath);
    }

    @Override
    public <T> T convertIfNecessary(Object value, Class<T> requiredType) throws TypeMismatchException {
        return getTypeConverter().convertIfNecessary(value, requiredType);
    }

    @Override
    public <T> T convertIfNecessary(Object value, Class<T> requiredType, MethodParameter methodParam)
            throws TypeMismatchException {
        return getTypeConverter().convertIfNecessary(value, requiredType, methodParam);
    }

    @Override
    public <T> T convertIfNecessary(Object value, Class<T> requiredType, Field field)
            throws TypeMismatchException {
        return getTypeConverter().convertIfNecessary(value, requiredType, field);
    }


    /**
     * Bind the given property values to this binder's target.
     * <p>This call can create field errors, representing basic binding
     * errors like a required field (code "required"), or type mismatch
     * between value and bean property (code "typeMismatch").
     * <p>Note that the given PropertyValues should be a throwaway instance:
     * For efficiency, it will be modified to just contain allowed fields if it
     * implements the MutablePropertyValues interface; else, an internal mutable
     * copy will be created for this purpose. Pass in a copy of the PropertyValues
     * if you want your original instance to stay unmodified in any case.
     * @param pvs property values to bind
     * @see #doBind(org.springframework.beans.MutablePropertyValues)
     */
    public void bind(PropertyValues pvs) {
        MutablePropertyValues mpvs = (pvs instanceof MutablePropertyValues) ?
                (MutablePropertyValues) pvs : new MutablePropertyValues(pvs);
        doBind(mpvs);
    }

    /**
     * Actual implementation of the binding process, working with the
     * passed-in MutablePropertyValues instance.
     * @param mpvs the property values to bind,
     * as MutablePropertyValues instance
     * @see #applyPropertyValues
     */
    protected void doBind(MutablePropertyValues mpvs) {
        applyPropertyValues(mpvs);
    }

    /**
     * Apply given property values to the target object.
     * <p>Default implementation applies all of the supplied property
     * values as bean property values. By default, unknown fields will
     * be ignored.
     * @param mpvs the property values to be bound (can be modified)
     * @see #getTarget
     * @see #getPropertyAccessor
     * @see #isIgnoreUnknownFields
     * @see #getBindingErrorProcessor
     * @see BindingErrorProcessor#processPropertyAccessException
     */
    protected void applyPropertyValues(MutablePropertyValues mpvs) {
        try {
            // Bind request parameters onto target object.
            getPropertyAccessor().setPropertyValues(mpvs, isIgnoreUnknownFields(), isIgnoreInvalidFields());
        }
        catch (PropertyBatchUpdateException ex) {
            // Use bind error processor to create FieldErrors.
            for (PropertyAccessException pae : ex.getPropertyAccessExceptions()) {
                getBindingErrorProcessor().processPropertyAccessException(pae, getInternalBindingResult());
            }
        }
    }
    /**
     * Inner class to avoid a hard dependency on Java 8.
     */
    @UsesJava8
    private static class OptionalUnwrapper {

        public static Object unwrap(Object optionalObject) {
            Optional<?> optional = (Optional<?>) optionalObject;
            if (!optional.isPresent()) {
                return null;
            }
            Object result = optional.get();
            Assert.isTrue(!(result instanceof Optional), "Multi-level Optional usage not supported");
            return result;
        }
    }

}
